iT邦幫忙

2023 iThome 鐵人賽

DAY 26
1

在一開始我們就介紹了 聯合類型和交叉類型 這兩種常用到的高級型別,威爾豬自己也是比較常使用它們。這次我們來看看還有哪些高級型別可以使用:

映射類型 ( Mapped Types )

映射類型是一種 泛型類型,根據一個現有型別的每個屬性都映射為另一個新型別,這在物件的屬性進行轉換或修改時特別有用。

interface IUser {
  id: number;
  name: string;
  greet: () => void;
}

type TMapped<T> = {
  [Property in keyof T]: boolean;
};

type TUserMapped = TMapped<IUser>;

const userMapped: TUserMapped = {
  id: true,
  name: true,
  greet: false,
};

在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並將其值變更為布林值,然後賦予到新增的 TUserMapped 類型別名上。

https://ithelp.ithome.com.tw/upload/images/20230926/20141250n25DVeutHP.png

  • 映射修改器 ( Mapping Modifiers )

在映射期間我們可以使用兩個修飾符:readonly?,並透過前綴符號 -+ 來刪除或新增這兩個修飾符,如果不加上前綴,則 + 為預設。

interface IReadonlyUser {
  readonly id: number;
  readonly name: string;
}

// readonly 加入前綴符號 -
type TMapped<T> = {
  -readonly [Property in keyof T]: T[Property];
};

type TUnlockUser = TMapped<TReadonlyUser>;

const unlockUser: TUnlockUser = {
  id: 1,
  name: "威爾豬",
};

unlockUser.name = "威爾羊";

在此範例中,TMapped 類型別名從 IReadonlyUser 接口中取得所有屬性 T,並刪除所有 readonly,然後賦予到新增的 TUnlockUser 類型別名上,這樣類型為 TUnlockUser 的物件屬性值就可以進行修改。

https://ithelp.ithome.com.tw/upload/images/20230926/20141250EogqgY2EjV.png

我們再看另一個範例:

interface IUser {
  id: string;
  name?: string;
  age?: number;
}

// 可選屬性加入前綴符號 -
type TMapped<T> = {
  [Property in keyof T]-?: T[Property];
};

type TMappedUser = TMapped<IUser>;

在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並刪除所有可選屬性,然後賦予到新增的 TMappedUser 類型別名上,這樣類型為 TMappedUser 的物件屬性就必須都存在。

https://ithelp.ithome.com.tw/upload/images/20230926/20141250RgX3zRA8bJ.png

  • 透過 as 重新映射類型

使用方式:

type TMapped<T> = {
  [Properties in keyof T as NewKeyType]: T[Properties];
};

看以下範例:

interface IUser {
  name: string;
  age: number;
  location: string;
}

type TMapped<T> = {
  [Property in keyof T as `get${Capitalize<
    string & Property
  >}`]: () => T[Property];
};

type TMappedUser = TMapped<IUser>;

在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並使用模板字面值加入 get 和將屬性開頭改為大寫,然後賦予到新增的 TMappedUser 類型別名上,這樣類型為 TMappedUser 的物件屬性就全部重新映射改為方法了。

https://ithelp.ithome.com.tw/upload/images/20230926/20141250XBec2JKCi1.png

當然如果我們有只想保留或排除的屬性時 ( 類似之前提到的 Pick 和 Omit ),也可以使用 as 來重新映射類型:

interface IUser {
  id: string;
  name: string;
  age: number;
  address: string;
  job: string;
}

// 使用 Extract 關鍵字保留屬性
type TMappedExtract<Type> = {
  [Property in keyof Type as Extract<
    Property,
    "id" | "name" | "job"
  >]: Type[Property];
};

// 使用 Exclude 關鍵字排除屬性
type TMappedExclude<Type> = {
  [Property in keyof Type as Exclude<
    Property,
    "id" | "address" | "job"
  >]: Type[Property];
};

type TMappedExtractUser = TMappedExtract<IUser>;
type TMappedExcludeUser = TMappedExclude<IUser>;

https://ithelp.ithome.com.tw/upload/images/20230926/20141250SFmKjSAoWM.png

https://ithelp.ithome.com.tw/upload/images/20230926/20141250erVvXPfVH3.png

索引類型 ( Index Types )

索引類型使用字串或數字索引來獲取和設置物件的屬性值,並根據現有物件的屬性創建新的型別。

看以下範例:

interface IPerson {
  [key: string]: string | number; // 使用索引類型
  gender: "male" | "female" | "others";
}

type TPersonKeys = keyof IPerson;

const getProperty = (obj: IPerson, key: TPersonKeys): void => {
  console.log(obj[key]);
};

const person: IPerson = {
  id: 1,
  name: "威爾豬",
  age: 3,
  gender: "male",
};

const personName = getProperty(person, "name"); // 輸出: 威爾豬
const personAddress = getProperty(person, "address"); // 輸出: undefined

在這個範例中,[key: string]: string | number 表示物件可以具有任意字符串索引,用於 getProperty 函數的參數 key 的型別。

條件類型 ( Conditional Types )

條件類型根據條件選擇不同的型別,使用 extends 關鍵字來定義條件。

type TIsString<T> = T extends string ? true : false;

type TStringType = TIsString<string>; // true
type TNumberType = TIsString<number>; // false

這邊不要把 接口 ( interface ) 的 extends 給弄混了,雖然它們在概念上是類似的,但 條件類型中的 extends 用於條件型別的選擇,而 interface 中的 extends 用於接口的繼承。儘管它們都包含 extends 這個關鍵字,但它們的功能和含義完全不同。

範圍類型 ( Template Literal Types )

創建 具有模板文字的型別

type TColor = "red" | "green" | "blue";
type TCssColor = `bg-${TColor}`;

https://ithelp.ithome.com.tw/upload/images/20230926/20141250a28oY15R4W.png

可變類型(Variadic Types)

可變類型是一種 允許函數參數的數量和型別不固定的類型,也就是可以接受任意數量型別的函數或類型,通常使用在泛型型別中,以便處理具有可變參數數量的情況。

看以下範例:

// 使用 ... 來表示可變數量的泛型參數
type Tuple<T extends unknown[]> = [...T];

const tupleArr1: Tuple<[number, string]> = [1, "哈囉"];
const tupleArr2: Tuple<[number, string, boolean]> = [3, "威爾豬", true];

在這個範例中,Tuple 是一個可變類型,它使用了 [...T],表示它接受任意數量的型別參數 T,可以根據提供的型別參數數量組成不同長度的元組型別。

看另一個範例:

const sum = <T extends number[]>(...args: T): number => {
  return args.reduce((acc, cur) => acc + cur, 0);
};

console.log(sum(1, 2, 3)); // 輸出: 6
console.log(sum(1, 2, 3, 4, 5)); // 輸出: 15

在這個範例中,sum 函式參數使用了可變類型,表示它接受任意數量的數字參數,並返回它們的總和。

字面量類型(Literal Types)

字面量類型可以 確保變數只能被賦值為特定的字面值,而不僅僅是一個範圍,這對於指定特定的字串、數字等非常有用,從而增強型別的安全性。

type TDirection = "up" | "down" | "left" | "right";

const userMove = (direction: TDirection): void => {
  console.log(`使用者向 ${direction} 移動`);
};

userMove("left"); // 輸出: 使用者向 left 移動

當然我們可以使用高級型別來做結合,以實現更靈活的型別操作。通常映射類型和索引類型會一起使用,以動態生成新型別。

interface IUser {
  id: number;
  name: string;
}

// 使用映射類型和索引類型
type TUpdateUser<T, U extends keyof T> = {
  [Property in U]?: T[Property];
};

const updateUser = <T, U extends keyof T>(
  user: T,
  updateObj: TUpdateUser<T, U>
): T => {
  return { ...user, ...updateObj };
};

const user: IUser = { id: 1, name: "威爾豬" };
const updatedUser = updateUser(user, { name: "威爾羊" });

console.log(updatedUser); // 輸出: {id: 1, name: '威爾羊'}

這段程式碼中,我們使用了映射類型和索引類型的組合,創建了一個類別別名 TUpdateUser。這個類別別名使用 keyof T 來獲取型別 T 中所有的屬性,並將其映射到一個新的類型別名 TUpdateUser 上。TUpdateUser 型別可以使用 U 中的每個屬性,並將這些屬性的值設定為 T 中對應屬性的可選項目。


高級型別能夠幫助我們更靈活地處理複雜的型別需求,使程式碼更具彈性和維護性,端看我們如何結合和運用。通過運用這些高級型別,我們可以在開發過程中更精確地定義型別,能夠更好地捕捉程式碼中的錯誤。


上一篇
型別縮小 ( Narrowing )
下一篇
Utility 型別 Ⅱ
系列文
用不到 30 天學會基本 TypeScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言